﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;

namespace FUSE.AzureConfig.Tasks
{
    public class SpecExtractor
    {
        public AzureConfigSpec Extract(string configDir)
        {
            if (string.IsNullOrEmpty(configDir) || !Directory.Exists(configDir))
                throw new ArgumentException(configDir + " does not exist");
            var csdef = Path.Combine(configDir, FileGenerator.CsdefName);
            if (!File.Exists(csdef))
                throw new ArgumentException(csdef + " does not exist");

            var csdefDoc = XDocument.Load(csdef);
            var cscfgs = GetCscfgsInDir(configDir);
            return SpecFromDocs(csdefDoc, cscfgs);
        }

        public AzureConfigSpec SpecFromDocs(XDocument csdef, IDictionary<string, XDocument> cscfgsByEnvname)
        {
            var settingsByRole = ProcessCsdef(csdef);
            var envs = ProcessCscfgs(cscfgsByEnvname);
            envs = MergeRoles(envs);
            return MergeEnvs(envs);
        }

        private IDictionary<string, ISet<string>> ProcessCsdef(XDocument csdefDoc)
        {
            var settingsByRole = new Dictionary<string, ISet<string>>();
            var rootNS = FileGenerator.GetRootNamespace(csdefDoc);
            var roles = from e in csdefDoc.Descendants()
                        where e.Name == rootNS + "WebRole" || e.Name == rootNS + "WorkerRole"
                        select e;
            foreach (var roleElt in roles)
            {
                var roleName = roleElt.Attribute("name").Value;
                var curSettings = new HashSet<string>();
                settingsByRole[roleName] = curSettings;
                var settingElts = roleElt.Element(rootNS + "ConfigurationSettings").Elements(rootNS + "Setting");
                foreach (var setting in settingElts)
                {
                    var settingName = setting.Attribute("name").Value;
                    curSettings.Add(settingName);
                }
            }
            return settingsByRole;
        }

        private static readonly Regex _CscfgNamePat = new Regex(@"ServiceConfiguration\.(?<env>[^.]+)\.cscfg");

        private class EnvData
        {
            public EnvData()
            {
                this.InstancesByRole = new Dictionary<string, int>();
                this.SettingsByRole = new Dictionary<string, Dictionary<string, string>>();
                this.CertsByRole = new Dictionary<string, Dictionary<string, Tuple<string, string>>>();
            }

            public string Name { get; set; }

            public Dictionary<string, int> InstancesByRole { get; set; }

            public Dictionary<string, Dictionary<string, string>> SettingsByRole { get; set; }

            public Dictionary<string, Dictionary<string, Tuple<string, string>>> CertsByRole { get; set; }
        }

        private IDictionary<string, XDocument> GetCscfgsInDir(string cscfgsDir)
        {
            var cscfgs = new Dictionary<string, XDocument>();
            var files = Directory.GetFiles(cscfgsDir, "*.cscfg");
            foreach (var file in files)
            {
                var envName = _CscfgNamePat.Match(file).Groups["env"].Value;
                envName = string.IsNullOrEmpty(envName) ? FileGenerator.DefaultEnvName : envName;
                cscfgs[envName] = XDocument.Load(file);
            }
            return cscfgs;
        }

        private List<EnvData> ProcessCscfgs(IDictionary<string, XDocument> cscfgs)
        {
            var envs = new List<EnvData>();
            foreach (var cscfg in cscfgs)
            {
                var envName = cscfg.Key;
                var env = new EnvData() { Name = envName };
                envs.Add(env);
                var doc = cscfg.Value;
                var rootNS = FileGenerator.GetRootNamespace(doc);
                var roles = from e in doc.Descendants()
                            where e.Name == rootNS + "Role"
                            select e;
                foreach (var roleElt in roles)
                {
                    var roleName = roleElt.Attribute("name").Value;
                    env.SettingsByRole[roleName] = new Dictionary<string, string>();
                    env.CertsByRole[roleName] = new Dictionary<string,Tuple<string,string>>();
                    env.InstancesByRole[roleName] = int.Parse(roleElt.Element(rootNS + "Instances").Attribute("count").Value);
                    var settingElts = roleElt.Element(rootNS + "ConfigurationSettings").Elements(rootNS + "Setting");
                    foreach (var setting in settingElts)
                    {
                        var settingName = setting.Attribute("name").Value;
                        var settingVal = setting.Attribute("value").Value;
                        env.SettingsByRole[roleName][settingName] = settingVal;
                    }
                    var certsElt = roleElt.Element(rootNS + "Certificates");
                    if (certsElt != null)
                    {
                        var certElts = certsElt.Elements(rootNS + "Certificate");
                        foreach (var cert in certElts)
                        {
                            var certName = cert.Attribute("name").Value;
                            var certTP = cert.Attribute("thumbprint").Value;
                            var certTPA = cert.Attribute("thumbprintAttribute");
                            env.CertsByRole[roleName][certName] = Tuple.Create(certTP, certTPA == null ? "sha1" : certTPA.Value);
                        }
                    }
                }
            }
            return envs;
        }

        private List<EnvData> MergeRoles(List<EnvData> envs)
        {
            var result = new List<EnvData>();
            foreach (var env in envs)
            {
                var newEnv = new EnvData() { Name = env.Name, InstancesByRole = env.InstancesByRole };
                var commonSettings = new Dictionary<Tuple<string, string>, List<string>>();
                foreach (var role in env.SettingsByRole)
                {
                    var rn = role.Key;
                    foreach (var setting in role.Value)
                    {
                        var key = Tuple.Create(setting.Key, setting.Value);
                        List<string> rolesForKey;
                        if (!commonSettings.TryGetValue(key, out rolesForKey))
                        {
                            rolesForKey = new List<string>();
                            commonSettings[key] = rolesForKey;
                        }
                        rolesForKey.Add(rn);
                    }
                }

                var mergedSettingsByRole = new Dictionary<string, Dictionary<string, string>>();
                foreach (var setting in commonSettings)
                {
                    var roleKey = string.Join(", ", setting.Value);
                    Dictionary<string, string> settingsForRoles;
                    if (!mergedSettingsByRole.TryGetValue(roleKey, out settingsForRoles))
                    {
                        settingsForRoles = new Dictionary<string, string>();
                        mergedSettingsByRole[roleKey] = settingsForRoles;
                    }
                    settingsForRoles[setting.Key.Item1] = setting.Key.Item2;
                }

                var commonCerts = new Dictionary<Tuple<string, string, string>, List<string>>();
                foreach (var role in env.CertsByRole)
                {
                    var rn = role.Key;
                    foreach (var cert in role.Value)
                    {
                        var key = Tuple.Create(cert.Key, cert.Value.Item1, cert.Value.Item2);
                        List<string> rolesForKey;
                        if (!commonCerts.TryGetValue(key, out rolesForKey))
                        {
                            rolesForKey = new List<string>();
                            commonCerts[key] = rolesForKey;
                        }
                        rolesForKey.Add(rn);
                    }
                }

                var mergedCertsByRole = new Dictionary<string, Dictionary<string, Tuple<string, string>>>();
                foreach (var cert in commonCerts)
                {
                    var roleKey = string.Join(", ", cert.Value);
                    Dictionary<string, Tuple<string, string>> certsForRoles;
                    if (!mergedCertsByRole.TryGetValue(roleKey, out certsForRoles))
                    {
                        certsForRoles = new Dictionary<string, Tuple<string, string>>();
                        mergedCertsByRole[roleKey] = certsForRoles;
                    }
                    certsForRoles[cert.Key.Item1] = Tuple.Create(cert.Key.Item2, cert.Key.Item3);
                }

                newEnv.SettingsByRole = mergedSettingsByRole;
                newEnv.CertsByRole = mergedCertsByRole;
                result.Add(newEnv);
            }
            return result;
        }

        private AzureConfigSpec MergeEnvs(List<EnvData> envs)
        {
            var spec = new AzureConfigSpec();
            var parent = string.Empty;
            Dictionary<string, Dictionary<string, string>> prevSettings = null;
            Dictionary<string, Dictionary<string, Tuple<string,string>>> prevCerts = null;
            foreach (var env in envs.OrderBy(x => x.Name))
            {
                var envSpec = new EnvironmentSpec() { ProfileName = env.Name };
                spec.Environments[env.Name] = envSpec;
                foreach (var role in env.InstancesByRole)
                {
                    if (role.Value > 0)
                        envSpec.Roles[role.Key] = new RoleSpec() { Instances = role.Value };
                }
                envSpec.Parent = parent;
                // Only set settings different from "parent"
                foreach (var role in env.SettingsByRole)
                {
                    var settings = new Dictionary<string, string>();
                    envSpec.ConfigSettings[role.Key] = settings;
                    foreach (var setting in role.Value)
                    {
                        if (prevSettings == null || !prevSettings.ContainsKey(role.Key) 
                            || !prevSettings[role.Key].ContainsKey(setting.Key) || prevSettings[role.Key][setting.Key] != setting.Value)
                            settings[setting.Key] = setting.Value;
                    }
                }

                foreach (var role in env.CertsByRole)
                {
                    var certs = new Dictionary<string, CertSpec>();
                    envSpec.Certificates[role.Key] = certs;
                    foreach (var cert in role.Value)
                    {
                        if (prevCerts == null || !prevCerts.ContainsKey(role.Key)
                            || !prevCerts[role.Key].ContainsKey(cert.Key) || !prevCerts[role.Key][cert.Key].Equals(cert.Value))
                        {
                            certs[cert.Key] = new CertSpec() { Thumbprint = cert.Value.Item1, ThumbprintAlgorithm = cert.Value.Item2 };
                        }
                    }
                }

                parent = env.Name;
                prevSettings = env.SettingsByRole;
                prevCerts = env.CertsByRole;
            }
            return spec;
        }

        private bool SameCert(CertSpec lhs, Tuple<string,string> rhs)
        {
            return lhs.Thumbprint == rhs.Item1
                && lhs.ThumbprintAlgorithm == rhs.Item2;
        }
    }
}
